export interface RequestOptions extends RequestInit {
  baseUrl?: string;
  skipAuth?: boolean;
}

export class HttpError extends Error {
  constructor(
    message: string,
    public status: number,
    public body?: unknown,
  ) {
    super(message);
    this.name = 'HttpError';
  }
}

const normalizeUrl = (baseUrl: string | undefined, path: string) => {
  if (!baseUrl || path.startsWith('http')) {
    return path;
  }

  const trimmedBase = baseUrl.replace(/\/$/, '');
  const normalizedPath = path.startsWith('/') ? path : `/${path}`;
  return `${trimmedBase}${normalizedPath}`;
};

const parseResponseBody = async (response: Response) => {
  const contentType = response.headers.get('content-type') ?? '';

  if (contentType.includes('application/json')) {
    const payload = await response.text();
    return payload ? JSON.parse(payload) : null;
  }

  if (contentType.includes('text/')) {
    return response.text();
  }

  return response.blob();
};

const handleError = async (response: Response): Promise<never> => {
  let message: string | undefined;
  let payload: unknown;

  try {
    payload = await response.json();
    message =
      typeof payload === 'string'
        ? payload
        : (payload as { title?: string; message?: string })?.title ??
          (payload as { title?: string; message?: string })?.message ??
          JSON.stringify(payload);
  } catch {
    const text = await response.text();
    message = text || `Request failed with status ${response.status}`;
  }

  throw new HttpError(message ?? 'Request failed', response.status, payload);
};

export async function request<T = unknown>(path: string, options: RequestOptions = {}): Promise<T> {
  const headers = new Headers(options.headers ?? {});
  const token = options.skipAuth ? null : localStorage.getItem('token');

  if (token && !headers.has('Authorization')) {
    headers.set('Authorization', `Bearer ${token}`);
  }

  const url = normalizeUrl(options.baseUrl, path);
  const response = await fetch(url, { ...options, headers });

  if (response.status === 204) {
    return null as T;
  }

  if (response.ok) {
    return (await parseResponseBody(response)) as T;
  }

  if (response.status === 401 && !options.skipAuth) {
    window.location.href = '/login';
    throw new HttpError('Unauthorized', response.status);
  }

  return handleError(response);
}

export async function requestStream(path: string, body: unknown, options: RequestOptions = {}) {
  const headers = new Headers(options.headers ?? {});
  const token = options.skipAuth ? null : localStorage.getItem('token');

  if (token && !headers.has('Authorization')) {
    headers.set('Authorization', `Bearer ${token}`);
  }

  if (!headers.has('Content-Type')) {
    headers.set('Content-Type', 'application/json');
  }

  const url = normalizeUrl(options.baseUrl, path);
  const response = await fetch(url, {
    ...options,
    method: options.method ?? 'POST',
    headers,
    body: JSON.stringify(body),
  });

  if (!response.ok || !response.body) {
    const errorText = await response.text();
    throw new HttpError(errorText || 'Stream request failed', response.status);
  }

  const reader = response.body.getReader();
  return {
    async *[Symbol.asyncIterator]() {
      try {
        while (true) {
          const { done, value } = await reader.read();
          if (done) {
            return;
          }
          const chunk = new TextDecoder('utf-8').decode(value);
          const lines = chunk.split('\n').filter((line) => line.trim() !== '');
          for (const line of lines) {
            const payload = line.startsWith('data: ') ? line.substring(6) : line;
            yield JSON.parse(payload);
          }
        }
      } finally {
        reader.releaseLock();
      }
    },
  };
}

export const get = <T = unknown>(path: string, options?: RequestOptions) =>
  request<T>(path, { ...(options ?? {}), method: 'GET' });

export const del = <T = unknown>(path: string, options?: RequestOptions) =>
  request<T>(path, { ...(options ?? {}), method: 'DELETE' });

export const post = <T = unknown>(path: string, options?: RequestOptions) =>
  request<T>(path, { ...(options ?? {}), method: 'POST' });

export const put = <T = unknown>(path: string, options?: RequestOptions) =>
  request<T>(path, { ...(options ?? {}), method: 'PUT' });

export const postJson = <T = unknown>(path: string, data: unknown, options?: RequestOptions) => {
  const headers = new Headers(options?.headers ?? {});
  if (!headers.has('Content-Type')) {
    headers.set('Content-Type', 'application/json');
  }

  return post<T>(path, {
    ...options,
    headers,
    body: JSON.stringify(data),
  });
};

export const putJson = <T = unknown>(path: string, data: unknown, options?: RequestOptions) => {
  const headers = new Headers(options?.headers ?? {});
  if (!headers.has('Content-Type')) {
    headers.set('Content-Type', 'application/json');
  }

  return put<T>(path, {
    ...options,
    headers,
    body: JSON.stringify(data),
  });
};
